package de.dailab.graphworld.bean;

import de.dailab.graphworld.bean.memory.NodeInfo;
import de.dailab.graphworld.bean.utils.BeanUtils;
import de.dailab.graphworld.ontology.ContractNetProtocol;
import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.GraphWorldException;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Proposal;
import de.dailab.graphworld.ontology.Reply;
import de.dailab.graphworld.ontology.ReplyType;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.pathfinding.AStar;
import de.dailab.graphworld.world.ExtendedGraphWorldAgentBean;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

/**
 * The carrier agent.
 *
 * @author Kseniya
 */
public class Carrier extends Contractor {

    private IActionDescription actionTakeGold;
    private IActionDescription actionDropGold;
    private IActionDescription actionMove;
    private final Queue<ActionContainer> taskActions = new LinkedList<>();
    private final Queue<Task> queuedTasks = new LinkedList<>();
    private Integer maxCapacity;
    private int goldAmountCarrying = 0;
    // TODO this flag is obsolete
    private boolean actionsOutdated = false;

    @Override
    public void doStart() {
        super.doStart();
        actionMove = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_MOVE));
        actionTakeGold = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD));
        actionDropGold = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD));
    }

    @Override
    public void execute() {
        super.execute();

        // TODO check if necessary now (can be removed)
        if (actionsOutdated) {
            planActions();
        }

        // TODO check if necessary now (can be removed)
        if (!queuedTasks.isEmpty() && taskActions.isEmpty()) {
            planActions();
        }

        if (!taskActions.isEmpty()) {
            ActionContainer nextAction = taskActions.peek();
            nextAction.perform();
        }
    }

    @Override
    public void receiveResult(ActionResult result) {
        String actionName = result.getAction().getName();
        if (result.getFailure() != null) {
            switch (actionName) {
                // no gold was found
                case ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD:
                    if (result.getFailure() instanceof GraphWorldException) {
                        GraphWorldException failure = (GraphWorldException) result.getFailure();
                        String emptyNodeMsg = "location does not contain any gold";
                        if (failure.getMessage().startsWith(emptyNodeMsg)) {
                            // remove failed gold take action and proceed
                            taskActions.poll();
                            consumeTakeGold(0);
                        }
                    }
                    break;
                // move failed
                case ExtendedGraphWorldAgentBean.ACTION_MOVE:
                    if (result.getFailure() instanceof GraphWorldException) {
                        GraphWorldException failure = (GraphWorldException) result.getFailure();
                        if (failure.getMessage().startsWith("Target position is not a neighbour")) {
                            // replan actions again
                            actionsOutdated = true;
                        }
                    }
                    break;
                default:
                    log.error(actionName);
                    log.error(result.getFailure());
                    log.error("curr node: " + memory.read(new NodeInfo(null)).getNode().getID());
                    break;
            }
        } else {
            taskActions.poll();
            switch (actionName) {
                case ExtendedGraphWorldAgentBean.ACTION_MOVE:
                    Node curr = (Node) result.getResults()[0];
                    consumeMove(curr);
                    // move action will be executed automatically by the next execute call
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD:
                    int goldTaken = ((Integer) result.getResults()[0]).intValue();
                    consumeTakeGold(goldTaken);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD:
                    consumeDropGold();
                    break;
                default:
                    break;
            }
        }
        super.receiveResult(result);
    }

    private void consumeMove(Node movedTo) {
        memory.update(new NodeInfo(null), new NodeInfo(movedTo));
        if (taskActions.isEmpty()) {
            actionsOutdated = true;
        } else {
            // take and drop gold can be performed immediately!
            ActionContainer nextAction = taskActions.element();
            if (nextAction.action == actionTakeGold) {
                nextAction.perform();
            } else if (nextAction.action == actionDropGold) {
                nextAction.perform();
            }
        }
    }

    private void consumeTakeGold(int goldTaken) {
        goldAmountCarrying += goldTaken;
        Node currPos = memory.read(new NodeInfo(null)).getNode();
        Collection<Task> completedTasks = BeanUtils.findTasksByNode(queuedTasks, currPos);
        ReplyType msgType = goldTaken > 0 ? ContractNetProtocol.INFORM : ContractNetProtocol.FAILURE;
        // inform broker about task success/failure and forget about it
        for (Task task : completedTasks) {
            sendMsgGroup(new Reply(task, msgType));
        }
        // remove completed/failed task
        queuedTasks.removeAll(completedTasks);
        actionsOutdated = true;
    }

    private void consumeDropGold() {
        // no actions, no tasks left - we are going idle now until the next call for proposals
        taskActions.clear();
        queuedTasks.clear();
        // we do not carry any gold anymore
        goldAmountCarrying = 0;
    }

    /**
     * Plans the actions sequence to perform all the queued tasks.
     */
    private void planActions() {
        // if carrying gold and all tasks completed, go drop that gold!
        if (queuedTasks.isEmpty() && goldAmountCarrying > 0) {
            planPathToHome();
            return;
        }
        // we are idle since nothing to plan
        if (queuedTasks.isEmpty()) {
            return;
        }
        Node currPos = memory.read(new NodeInfo(null)).getNode();
        // start with the nearest task
        Task best = selectNearestTask(queuedTasks, currPos);
        taskActions.clear();
        taskActions.addAll(planActions(best, currPos));
        actionsOutdated = false;
    }

    /**
     * Plans actions to perform the given task from the given node.
     *
     * @param task to be performed
     * @param from initial position
     * @return action sequence to execute
     */
    private List<ActionContainer> planActions(Task task, Node from) {
        // calc path to the task's gold node first
        List<Edge> pathToTask;
        if (from.getID() == task.goldNode.getID()) {
            pathToTask = Collections.emptyList();
        } else {
            pathToTask = AStar.findPath(from, task.goldNode);
        }
        // enqueue the needed actions
        List<ActionContainer> actions = new ArrayList<>();
        for (Edge edge : pathToTask) {
            actions.add(new ActionContainer(actionMove, new Serializable[]{edge.getTarget()}));
        }
        actions.add(new ActionContainer(actionTakeGold, null));
        return actions;
    }

    /**
     * Plans the move actions to the home node and one drop action to get rid of
     * the gold.
     */
    private void planPathToHome() {
        Node curr = memory.read(new NodeInfo(null)).getNode();
        List<Edge> pathToHome;
        if (curr.getID() == 0) {
            pathToHome = Collections.emptyList();
        } else {
            pathToHome = AStar.findPath(curr, 0);
        }
        List<ActionContainer> actions = new ArrayList<>();
        for (Edge edge : pathToHome) {
            actions.add(new ActionContainer(actionMove, new Serializable[]{edge.getTarget()}));
        }
        actions.add(new ActionContainer(actionDropGold, null));
        taskActions.clear();
        taskActions.addAll(actions);
    }

    /**
     * Selects the task which gold node is the nearest to our position.
     *
     * @param tasks some tasks to select from
     * @param from node where we currently are
     * @return the nearest task
     */
    private Task selectNearestTask(Collection<Task> tasks, Node from) {
        // calc paths to all tasks' nodes first
        Map<Task, List<Edge>> paths = new HashMap<>();

        for (Task task : tasks) {
            Node target = task.goldNode;
            List<Edge> path;
            if (from.getID() == target.getID()) {
                path = Collections.emptyList();
            } else {
                path = AStar.findPath(from, target);
            }
            paths.put(task, path);
        }
        // now select task with the shortest path
        Task nearest = null;
        int minDist = Integer.MAX_VALUE;
        for (Map.Entry<Task, List<Edge>> entry : paths.entrySet()) {
            if (minDist > entry.getValue().size()) {
                minDist = entry.getValue().size();
                nearest = entry.getKey();
            }
        }
        return nearest;
    }

    private int getFreeCapacity() {
        return Math.max(0, maxCapacity - goldAmountCarrying);
    }

    public Integer getMaxCapacity() {
        return maxCapacity;
    }

    public void setMaxCapacity(Integer maxCapacity) {
        this.maxCapacity = maxCapacity;
    }

    @Override
    protected boolean canAcceptTask(Task task) {
        return calcRestCapacity() > 0;
    }

    @Override
    protected Proposal createProposal(Task task) {
        // two things must be passed to the scout:

        // the path to task's node...
        Node currPos = memory.read(new NodeInfo(null)).getNode();
        List<Edge> path;
        if (currPos.getID() == task.goldNode.getID()) {
            path = Collections.emptyList();
        } else {
            path = AStar.findPath(currPos, task.goldNode);
        }
        // ...and how much gold we can grab from task's node!
        int restCapacity = calcRestCapacity();
        return new Proposal(task, restCapacity, path);
    }

    private int calcRestCapacity() {
        // calculate free capacity carrier can offer
        int goldQueued = 0;
        for (Task queuedTask : queuedTasks) {
            goldQueued += queuedTask.goldNode.getGold();
        }
        // it is simply the capacity that is currently available minus
        // the gold that is queued to be collected
        return getFreeCapacity() - goldQueued;
    }

    @Override
    protected void consumeAcceptance(Task task) {
        // enqueue the task
        queuedTasks.offer(task);
        actionsOutdated = true;
    }

    @Override
    protected void consumeRejection(Task task) {
        // do nothing since out internal state won't change at all
    }

    private class ActionContainer {

        private final IActionDescription action;
        private final Serializable[] params;

        public ActionContainer(IActionDescription action, Serializable[] params) {
            this.action = action;
            this.params = params;
        }

        void perform() {
            Serializable[] actionParams;
            if (action == Carrier.this.actionTakeGold) {
                // always have the free capacity up to date
                actionParams = new Serializable[]{Integer.valueOf(Carrier.this.getFreeCapacity())};
            } else {
                actionParams = params;
            }
            Carrier.this.invoke(action, actionParams, Carrier.this);
        }

        @Override
        public String toString() {
            String params2Str = "";
            if (params != null && params.length > 0 && params[0] != null) {
                params2Str += ((Node) params[0]).getID();
            }
            return action.getName() + " " + params2Str;
        }
    }
}
